//
// Descriptors.cs
//
// Author: 
//  Fredy Ramirez <framirez@pacificcoolwave.com>
//
// Copyright (C) 2011 Fredy Ramirez (http://www.pacificcoolwave.com)
//
//  Creation date: Oct 12, 2011
//  Last modification: 

/*
The MIT License

Copyright (C) 2011 Fredy Ramirez (http://www.pacificcoolwave.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 */


using System;
using System.Xml;
using System.Collections.Generic;
using Gtk;
using System.Text.RegularExpressions;
using Pcw.PcwTextEditor.PcwDocument;

namespace Pcw.Addins.Highlighting.Definition
{
	internal enum HighlightType 
	{
		None,
		WholeWord,
		Contains,
		ToCloseToken,	// | Considered as block type and need to be manipulated in special form.
		ToEOL			// | Each block have a HighlightBlock object.
	}
	
	internal class Descriptors
	{
		private List<Descriptor> descriptors = new List<Descriptor> ();				// All descriptors
		private List<Descriptor> blocks_descriptors = new List<Descriptor> ();		// ToCloseToken, ToEOL
		private List<Descriptor> non_blocks_descriptors = new List<Descriptor> ();	// WholeWord, Contains
		
		private Separators separators;
		
		internal List<Descriptor> DescriptorList {
			get {
				return descriptors;
			}
		}
		
		internal Descriptors (XmlNode descriptorsNode, Styles styles, Separators separators, bool caseSensitive)
		{
			if (styles == null)
				throw new InvalidOperationException("No styles found in Pcw.Addins.Highlighting");
			
			this.separators = separators;
			
			Style style;
			HighlightType type;
			
			foreach (XmlNode n in descriptorsNode.ChildNodes) 
			{
				if (n.NodeType == XmlNodeType.Comment)
					continue;

				XmlElement item = (XmlElement)n;
					
				style = styles[XmlUtil.GetString(item, "style")];
				type = XmlUtil.GetHighlightType(XmlUtil.GetString(item, "type"));				
				
				Descriptor dsp;
				
				if (type == HighlightType.ToCloseToken || type == HighlightType.ToEOL) {
					// We need one descriptor for each token
					if (item.ChildNodes.Count > 0) {
						XmlNode childNode = item.ChildNodes[0];
						foreach (XmlNode cn in childNode.ChildNodes) 
						{
							if (cn.NodeType == XmlNodeType.Comment)
								continue;
							
							XmlElement itemNode = (XmlElement)cn;
							dsp = new Descriptor(style, type, XmlUtil.GetString(itemNode, "value"), 
													XmlUtil.GetString(itemNode, "closeToken"), 
													XmlUtil.GetString(itemNode, "escapeToken"), 
													XmlUtil.GetBoolean(itemNode, "multiLine"),
													caseSensitive);
							
							descriptors.Add(dsp);
							blocks_descriptors.Add(dsp);
						}
					}
				}
				else { // non_blocks type
					dsp = new Descriptor(n, style, type, caseSensitive);
					descriptors.Add(dsp);
					non_blocks_descriptors.Add(dsp);
				}
			}
		}
		
		internal void ApplyHighlightLoading(TextBuffer buffer, HighlightBlocks blockList) 
		{
			if (IsSeparator(buffer.Text)) 
				return;
			
			blockList = GetBlockList(buffer, blockList);
			
			if (blockList.Blocks.Count == 0)
			{
				ApplyNonBlockTags(buffer, 0, buffer.EndIter.Offset, blockList);
				return;
			}
			
			int nextStartOffset = 0;
			int lastOffset = -1;
			int index = 0;
			
			foreach (var pair in blockList.Blocks) {
				
				if(pair.Value.Status == BlockStatus.AllDeleted)  // It was deleted in a ApplyNonBlockTags() process
					continue;
				
				if (pair.Value.StartOffset == nextStartOffset) {
					nextStartOffset = pair.Value.EndOffset + 1;
					index++;
					if (index == blockList.Blocks.Count && pair.Value.EndOffset < buffer.EndIter.Offset) { // Text after last block
						ApplyNonBlockTags(buffer, nextStartOffset, buffer.EndIter.Offset, blockList);
						break;
					}
				    continue;
				}

				lastOffset = ApplyNonBlockTags(buffer, nextStartOffset, pair.Value.StartOffset - 1, blockList);
				
				if (lastOffset == buffer.EndIter.Offset)
					break; // Probably a multiline open block was found.
				
				if (lastOffset > pair.Value.StartOffset - 1) 
					nextStartOffset = lastOffset + 1;
				else
					nextStartOffset = pair.Value.EndOffset + 1;
				
				index++;
				
				if (index == blockList.Blocks.Count && pair.Value.EndOffset < buffer.EndIter.Offset) 
					ApplyNonBlockTags(buffer, nextStartOffset, buffer.EndIter.Offset, blockList);
				
			}			
			
			DeleteBlocksMarked(blockList);
			
			// Apply block tags
			ApplyBlockTags(buffer, blockList);

		}
		
		internal void ApplyHighlightChanged(TextBuffer buffer, TextChangedInfo info, HighlightBlocks blockList) 
		{
			AdjustAllOffset(info, blockList);
						
			if (blockList.Blocks.Count == 0) {
				ApplyNonBlockTags(buffer, 0, buffer.EndIter.Offset, blockList); 
				return;
			}
			
			string blockText; 
			HlBlock resultBlock;
			HlBlock beforeBlock;
			HlBlock afterBlock;
			
			BetweenBlockResult result = blockList.VerifyBetweenBlock(info, out resultBlock, out beforeBlock, out afterBlock);
			
			switch(result) 
			{
				case BetweenBlockResult.NoInBlock:				
					int start = 0;
					int end = buffer.EndIter.Offset;
					if (beforeBlock != null)
						start = beforeBlock.EndOffset + 1;
					if(afterBlock != null)
						end = afterBlock.StartOffset - 1;
					ApplyNonBlockTags(buffer, start, end, blockList);
				
					break;
				
				case BetweenBlockResult.EndOpenBlock:
					// Apply tag to the new end
					ApplyTag(buffer, resultBlock.Descriptor.TagName, resultBlock.StartOffset, resultBlock.EndOffset);
				
					// Verify if closeToken is now present and change status to completed
					if (resultBlock.Descriptor.HlType == HighlightType.ToCloseToken)  {
						blockText = GetStringByOffset(buffer, resultBlock.StartOffset, resultBlock.EndOffset);
						if (blockText.EndsWith(resultBlock.Descriptor.CloseToken)) 
							resultBlock.Status = BlockStatus.Completed;
					}
				
					break;
				
				case BetweenBlockResult.InRange:
					/*
					if (resultBlock.Descriptor.HlType != HighlightType.ToCloseToken)
						break;
				
					// Verify if there is a new close token inside.
				
					startIter = buffer.StartIter;
					startIter.ForwardChars(resultBlock.StartOffset);
					endIter = buffer.StartIter;
					endIter.ForwardChars(resultBlock.EndOffset + info.Text.Length);					
					blockText = buffer.GetText(startIter, endIter, true);
						
					Match[] match = resultBlock.Descriptor.GetMatches(blockText);
					Console.WriteLine("CloseToken: {0}", resultBlock.StartOffset + match[0].Index + match.GetValue(0).ToString().Length);
					if (match.Length == 1 && 
						(resultBlock.EndOffset == resultBlock.StartOffset + match[0].Index + match.GetValue(0).ToString().Length))
						break;
					
					buffer.RemoveAllTags(startIter, endIter);
					ApplyTag(buffer, resultBlock.Descriptor.TagName, resultBlock.StartOffset, resultBlock.StartOffset + match[0].Index + match.GetValue(0).ToString().Length);
				
					resultBlock.Completed = true;
					resultBlock.EndOffset = resultBlock.StartOffset + match[0].Index + match.GetValue(0).ToString().Length;
				
					// TODO: Verify string after close token
				
					*/
				
					break;
				
				case BetweenBlockResult.BreakStartBlock:
					// Remove this block form line block list
				
					// Process line
				
					break;
				
				case BetweenBlockResult.BreakEndBlock:
					/*
				
					// Extend tag to the new open block end (EOL or EOF)
					if (!resultBlock.Descriptor.IsMultiLine){ // Extends to EOL
						blockText = GetStringToEoL(buffer, resultBlock.StartOffset, ref startIter, ref endIter);
						buffer.RemoveAllTags(startIter, endIter);
						ApplyTag(buffer, resultBlock.Descriptor.TagName, startIter.Offset, endIter.Offset);
					}
					else{ // Extends to EOF
					
					}
				
					// change endOffset and  block status to not completed
					resultBlock.Completed = false;
					resultBlock.EndOffset = endIter.Offset;	
				
					*/
				
					break;
				
				default:
					throw new NotImplementedException("BetweenResult '" + result.ToString() + "' is not implemented in ApplyHighlightChanged().");
			}

		}
		
		private bool IsSeparator(string text)
		{
			if (separators == null)
				throw new InvalidProgramException("Separators not found!. Verify if syntax file contains definitions for separators. - Pcw.Highlighting");
			
			text = text.Trim();
			
			if (text == "")
				return true;
			
			if (text.Length > 1)
				return false;			
			
			return separators.IsMatch(text);

		}
		
		private string GetStringByOffset(TextBuffer buffer, int startOffset, int endOffset)
		{
			TextIter startIter = buffer.StartIter;
			startIter.ForwardChars(startOffset);
			TextIter endIter = buffer.StartIter;
			endIter.ForwardChars(endOffset);
			return buffer.GetText(startIter, endIter, true);
		}
		
		private void RemoveTagByOffset(TextBuffer buffer, int startOffset, int endOffset)
		{
			RemoveTagByOffset(buffer, null, startOffset, endOffset);
		}
		
		private void RemoveTagByOffset(TextBuffer buffer, string tagName, int startOffset, int endOffset)
		{
			TextIter startIter = buffer.StartIter;
			startIter.ForwardChars(startOffset);
			TextIter endIter = buffer.StartIter;
			endIter.ForwardChars(endOffset);
			if (String.IsNullOrEmpty(tagName))
			    buffer.RemoveAllTags(startIter, endIter);
			else    
				buffer.RemoveTag(tagName, startIter, endIter);
		}
		
		private string GetStringToEoL(TextBuffer buffer, int startOffset, ref TextIter startIter, ref TextIter endIter) 
		{
			startIter = buffer.StartIter;
			startIter.ForwardChars(startOffset);
			endIter = startIter;
			endIter.ForwardLine();
			
			string s = buffer.GetText(startIter, endIter, true);
			s = ReduceStringEnd(s, "\n", ref endIter);
			s = ReduceStringEnd(s, "\r", ref endIter);
			
			return s;
		}
		
		private string ReduceStringEnd(string str, string strEvaluation, ref TextIter iter) 
		{
			if (str.EndsWith(strEvaluation)) {
				str = str.Substring(0, str.Length - 1);
				iter.BackwardChar();
			}
			return str;
		}
		
		// Get the list of blocks (BlockStatus.Completed) in buffer
		private HighlightBlocks GetBlockList (TextBuffer buffer, HighlightBlocks dList) 
		{
			if (dList == null) 
				dList = new HighlightBlocks();
			
			foreach(Descriptor d in blocks_descriptors) 
			{
				if (buffer.Text.IndexOf(d.Key) < 0)
					continue;
				
				Match[] match = d.GetMatches(buffer.Text);
				
				if (match != null) { 
					for (int x = 0; x < match.Length; x++) {
						dList.AddBlock(match[x].Index, new HlBlock(buffer, buffer.StartIter.Offset + match[x].Index, 
							buffer.StartIter.Offset + match[x].Index + match.GetValue(x).ToString().Length, BlockStatus.Completed, d));
					}
					continue;
				}
			}
		
			// Remove blocks we do not need (blocks inside other blocks)
			bool completed = false;
			while(!completed) {
				completed = true;
				foreach (var pair in dList.Blocks) 
				{
					if(BetweenBlock(dList, pair.Value.StartOffset)) {
						//Console.WriteLine("Removing block with key '{0}' startOffset {1} endOffset {2}", pair.Key, pair.Value.StartOffset, pair.Value.EndOffset);
						dList.Blocks.Remove(pair.Key); 
						completed = false;
						break;
					}
				}
			}
			
			/*
			// For testing - Show existing blocks
			Console.WriteLine("LIST OF BLOCKS");
			foreach (var pair in dList.Blocks) 
			{
				Console.WriteLine("Block '{0}' Text: {1}",pair.Key, GetStringByOffset(buffer, pair.Value.StartOffset, pair.Value.EndOffset));
			}
			*/
			
			return dList;
		}

		// This function deal with non blocks descriptors (text between two blocks), but 
		// it can deal with open block token and reorder the entire block list.
		// It returns the last endOffset affected. Usually it would be same passed as argument unless
		// open block key is present, then it returns EOL offset or EOF offset.
		private int ApplyNonBlockTags(TextBuffer buffer, int startOffset, int endOffset, HighlightBlocks blockList)
		{
			// Remove all tags in range.
			RemoveTagByOffset(buffer, startOffset, endOffset);
			string text = GetStringByOffset(buffer, startOffset, endOffset);
			
			if (IsSeparator(text))
				return endOffset;
			
			// Verify if there is an open key of a block. 
			SortedList<int, HlBlock> dList = new SortedList<int, HlBlock> ();
			
			foreach(Descriptor d in blocks_descriptors) {
				if (text.IndexOf(d.Key) > -1) {
					dList.Add(text.IndexOf(d.Key), new HlBlock(buffer, startOffset + text.IndexOf(d.Key), 
					-1, BlockStatus.Incompleted, d));
					//Console.WriteLine("New open block '{0}' found in text: '{1}'", d.Key, text);
				}
			}
			
			foreach (var pair in dList) {
				
				// There is text before block
				if(startOffset < pair.Value.StartOffset) {
					text = GetStringByOffset(buffer, startOffset, pair.Value.StartOffset - 1);
					if (!IsSeparator(text))
						foreach(Descriptor d in non_blocks_descriptors) 
							DoMatch(d.GetMatches(text), buffer, d.TagName, text, startOffset);
				}
				
				if (pair.Value.Descriptor.IsMultiLine) {
					// Tag until EOF
					pair.Value.EndOffset = buffer.EndIter.Offset;
				}
				else {
					// Tag until EOL
					TextIter lineStartIter, lineEndIter;
					GetStringToEoL(buffer, pair.Value.StartOffset, ref lineStartIter, ref lineEndIter); // Get the EOL offset
					pair.Value.EndOffset = lineEndIter.Offset;
				}
				
				ApplyTag(buffer, pair.Value.Descriptor.TagName, pair.Value.StartOffset, pair.Value.EndOffset);
				blockList.AddBlock(pair.Value.StartOffset, pair.Value);
				pair.Value.Status = BlockStatus.Incompleted;
				
				// To be deleted in calling process
				MarkBlocksForDelete(blockList, pair.Value.StartOffset, pair.Value.EndOffset);

				if (pair.Value.EndOffset == endOffset || pair.Value.Descriptor.IsMultiLine)
					return pair.Value.EndOffset; 
								
				startOffset = pair.Value.EndOffset + 1 ; 

			}
			
			text = GetStringByOffset(buffer, startOffset, endOffset);
			
			// apply non block tags after last open block
			foreach(Descriptor d in non_blocks_descriptors) 
				DoMatch(d.GetMatches(text), buffer, d.TagName, text, startOffset);
			
			return endOffset;

		}
		
		private void MarkBlocksForDelete(HighlightBlocks blockList, int startOffset, int endOffset) 
		{
			foreach(var pair in blockList.Blocks) 
				if (pair.Value.StartOffset > startOffset && pair.Value.EndOffset <= endOffset) 
					pair.Value.Status = BlockStatus.AllDeleted;
		}

		private void DeleteBlocksMarked(HighlightBlocks blockList) 
		{
			bool completed = false;
			while(!completed) 
			{
				completed = true;
				foreach(var pair in blockList.Blocks) 
				{
					if(pair.Value.Status == BlockStatus.AllDeleted) {
						blockList.Blocks.Remove(pair.Key);
						completed = false;
						break;
					}
				}
			}
		}		
		
		private void ApplyBlockTags(TextBuffer buffer, HighlightBlocks blockList)
		{
			foreach(var pair in blockList.Blocks) 
					ApplyTag(buffer, pair.Value.Descriptor.TagName, pair.Value.StartOffset, pair.Value.EndOffset);
		}
		
		private bool BetweenBlock(HighlightBlocks blockList, int startOffset) 
		{
			foreach(var pair in blockList.Blocks) 
				if (startOffset > pair.Value.StartOffset && startOffset < pair.Value.EndOffset)
					return true;
			
			return false;
		}
		
		private bool DoMatch(Match[] match, TextBuffer buffer, string tagName, string stringLine, int startOffset) 
		{
			if (match == null || match.Length < 1)
				return false;
					
			for (int x = 0; x < match.Length; x++)
				ApplyTag(buffer, tagName, startOffset + match[x].Index, startOffset + match[x].Index + match.GetValue(x).ToString().Length);
			
			return true;
			
		}

		private void ApplyTag(TextBuffer buffer, string tagName, int startOffset, int endOffset)
		{
			TextIter startIter = buffer.StartIter;
			startIter.ForwardChars(startOffset);
			TextIter endIter = buffer.StartIter;
			endIter.ForwardChars(endOffset);

			//Console.WriteLine("Applying tag {2} to text '{3}' - start: {0} - end: {1}", lineStartIter.Offset, endIter.Offset, tagName, text);
			buffer.RemoveAllTags(startIter, endIter);
			buffer.ApplyTag(tagName, startIter, endIter);
		}
		
		private void AdjustAllOffset(TextChangedInfo info, HighlightBlocks blockList) 
		{
			bool reorder = false;
			
			// Run AdjustOffset()			
			foreach(var pair in blockList.Blocks) {
				if (!pair.Value.AdjustOffset(info))
					reorder = true;
			}
			
			if (!reorder)
				return;
			
			// Remove blocks do not exist
			DeleteBlocksMarked(blockList);
			
		}
	}
}
